<!DOCTYPE html>
<html>
<head>
	<title>Browser Dialer</title>
</head>
<body>
	<script>
		"use strict";
		// Enable a much more aggressive JIT for performance gains

		// Copyright (c) 2021 XRAY. Mozilla Public License 2.0.
		let url = "ws://" + window.location.host + "/websocket?token=csrfToken";
		let clientIdleCount = 0;
		let upstreamGetCount = 0;
		let upstreamWsCount = 0;
		let upstreamPostCount = 0;
		let check = function () {
			if (clientIdleCount > 0) {
				return;
			};
			clientIdleCount += 1;
			console.log("Prepare", url);
			let ws = new WebSocket(url);
			// arraybuffer is significantly faster in chrome than default
			// blob, tested with chrome 123
			ws.binaryType = "arraybuffer";
			ws.addEventListener("message", (event) => {
				clientIdleCount -= 1;
				let [method, url, protocol] = event.data.split(" ");
				switch (method) {
					case "WS": {
						upstreamWsCount += 1;
						console.log("Dial WS", url, protocol);
						const wss = new WebSocket(url, protocol);
						wss.binaryType = "arraybuffer";
						let opened = false;
						ws.onmessage = function (event) {
							wss.send(event.data)
						};
						wss.onopen = function (event) {
							opened = true;
							ws.send("ok")
						};
						wss.onmessage = function (event) {
							ws.send(event.data)
						};
						wss.onclose = function (event) {
							upstreamWsCount -= 1;
							console.log("Dial WS DONE, remaining: ", upstreamWsCount);
							ws.close()
						};
						wss.onerror = function (event) {
							!opened && ws.send("fail")
							wss.close()
						};
						ws.onclose = function (event) {
							wss.close()
						};
						break;
					};
					case "GET": {
						(async () => {
							console.log("Dial GET", url);
							ws.send("ok");
							const controller = new AbortController();

							/*
							Aborting a streaming response in JavaScript
							requires two levers to be pulled:

							First, the streaming read itself has to be cancelled using
							reader.cancel(), only then controller.abort() will actually work.

							If controller.abort() alone is called while a
							reader.read() is ongoing, it will block until the server closes the
							response, the page is refreshed or the network connection is lost.
							*/

							let reader = null;
							ws.onclose = (event) => {
								try {
									reader && reader.cancel();
								} catch(e) {};

								try {
									controller.abort();
								} catch(e) {};
							};

							try {
								upstreamGetCount += 1;
								const response = await fetch(url, {signal: controller.signal});

								const body = await response.body;
								reader = body.getReader();

								while (true) {
									const { done, value } = await reader.read();
									ws.send(value);
									if (done) break;
								};
							} finally {
								upstreamGetCount -= 1;
								console.log("Dial GET DONE, remaining: ", upstreamGetCount);
								ws.close();
							};
						})();
						break;
					};
					case "POST": {
						upstreamPostCount += 1;
						console.log("Dial POST", url);
						ws.send("ok");
						ws.onmessage = async (event) => {
							try {
								const response = await fetch(
									url,
									{method: "POST", body: event.data}
								);
								if (response.ok) {
									ws.send("ok");
								} else {
									console.error("bad status code");
									ws.send("fail");
								};
							} finally {
								upstreamPostCount -= 1;
								console.log("Dial POST DONE, remaining: ", upstreamPostCount);
								ws.close();
							};
						};
						break;
					};
				};

				check();
			});
			ws.addEventListener("error", (event) => {
				ws.close();
			});
		};
		let checkTask = setInterval(check, 1000);
	</script>
</body>
</html>
